/**
 * 
 */
package lelouet.datacenter.simulation.hypervisors;

import java.util.*;

import lelouet.datacenter.simulation.*;
import lelouet.datacenter.simulation.Event.EventStates;
import lelouet.datacenter.simulation.events.*;
import lelouet.datacenter.simulation.events.VMMove.VMMoves;
import lelouet.datacenter.simulation.events.VMStateChanged.VMStates;
import lelouet.datacenter.simulation.factorized.WorkingElementImpl;
import lelouet.datacenter.simulation.vms.AVM;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * basic version of an hypervisor, that reacts to incoming events.<br />
 * <em>Note :</em>the {@link #hashCode()} and {@link #equals(Object)} refer to
 * the ids
 */
public class HypervisorImpl extends WorkingElementImpl implements EventHandler,
Hypervisor {

	private static final long serialVersionUID = 1L;

	private static final Logger mlogger = LoggerFactory
	.getLogger(HypervisorImpl.class);

	protected Logger log() {
		return mlogger;
	}

	/** construct an {@link Hypervisor} with no memory available and no CPU */
	public HypervisorImpl(String id, long time) {
		super(id, time);
	}

	public HypervisorImpl(String id, long time, long memSize, long mips) {
		this(id, time);
		setVMsDedicatedMem(memSize);
		setVMsDedicatedCPU(mips);
	}

	/**
	 * the VMs in that set are supposed to receive 0 MIPS for work.
	 */
	protected HashSet<VM> suspendedVMs;

	protected Set<VM> suspendedVMsUnModifiable;

	protected HashSet<VM> presentVms;

	protected Set<VM> presentVMsUnModifiable;

	{
		suspendedVMs = new HashSet<VM>();
		suspendedVMsUnModifiable = Collections.unmodifiableSet(suspendedVMs);
		presentVms = new HashSet<VM>();
		presentVMsUnModifiable = Collections.unmodifiableSet(presentVms);
	}

	/** the number of MIPS that hypervisor can assign to running its VMs */
	protected long VMsDedicatedCPU;

	@Override
	public void setVMsDedicatedCPU(long mips) {
		VMsDedicatedCPU = mips;
	}

	@Override
	public long getVMsDedicatedCPU() {
		return VMsDedicatedCPU;
	}

	/** the number of Mo of memory that hypervisor can assign to all its VMs */
	protected long VMsDedicatedMem;

	@Override
	public void setVMsDedicatedMem(long mem) {
		VMsDedicatedMem = mem;
	}

	@Override
	public long getVMsDedicatedMem() {
		return VMsDedicatedMem;
	}

	@Override
	public long getVMsAllocatedMem() {
		long allocated = 0;
		for (VM vm : presentVms) {
			allocated += vm.getMemUsed();
		}
		return allocated;
	}

	@Override
	public long nextEventTime() {
		if (hasNewTriggeredEvents()) {
			return getTime();
		}
		long ret = Long.MAX_VALUE;
		for (VM vm : presentVms) {
			long nextTime = vm.nextEventTime();
			if (nextTime < ret) {
				ret = nextTime;
			}
		}
		return ret;
	}

	/**
	 * compare two VM according to the amount of MIPS the hypervisor should
	 * share to fulfill the first. return <0 if o1 would be fulfilled first, 1
	 * if o2 would be fulfilled first, or 0 if equality.
	 */
	public static int earlyFulfillComparison(VM o1, VM o2) {
		double diff = o1.getMaxCPU() * o2.getWeightCPU() - o2.getMaxCPU()
		* o1.getWeightCPU();
		return diff == 0 ? 0 : diff > 0 ? 1 : -1;
	}

	/**
	 * a class for the algorithm of allocating resources. Handles the
	 * {@link AVM} s one after the other, and then allocate unused resources
	 */
	protected class ResourcesAllocation {

		public void restart() {
			weightsSum = 0;
			ranked.clear();
		}

		protected TreeSet<VM> ranked = new TreeSet<VM>(new Comparator<VM>() {

			@Override
			public int compare(VM o1, VM o2) {
				int ret = earlyFulfillComparison(o1, o2);
				if (ret == 0) {
					ret = o1.getId().compareTo(o2.getId());
				}
				return ret;
			}
		});

		private long weightsSum = 0;

		public void handleWorkingVM(VM vm) {
			weightsSum += vm.getWeightCPU();
			ranked.add(vm);
		}

		public void handleSuspendedVM(VM vm) {
			vm.setAllocatedCPU(0);
		}

		/**
		 * no more VM to handle : allocate the cpu first to easiest to fulfill
		 * vms, knowing that if one easiest to fulfill is not fulfill, the
		 * harder ones will neither be.
		 */
		public void finish() {
			long remainingMIPS = getVMsDedicatedCPU();
			for (VM vm : ranked) {
				double coeff = (double) vm.getWeightCPU() / weightsSum;
				long allocated = (long) Math.floor(coeff * remainingMIPS);
				if (allocated > vm.getMaxCPU()) {
					allocated = vm.getMaxCPU();
				}
				vm.setAllocatedCPU(allocated);
				remainingMIPS -= allocated;
				weightsSum -= vm.getWeightCPU();
			}
		}

	}

	private ResourcesAllocation myAllocating = new ResourcesAllocation();

	@Override
	public void reconfigure() {
		myAllocating.restart();
		for (VM vm : presentVms) {
			if (suspendedVMs.contains(vm)) {
				myAllocating.handleSuspendedVM(vm);
			} else {
				myAllocating.handleWorkingVM(vm);
			}
		}
		myAllocating.finish();
	}

	@Override
	public List<Event> work(long nextStop) {
		List<Event> ret = new ArrayList<Event>();
		if (hasNewTriggeredEvents()) {
			ret.addAll(getTriggeredEvents());
		}
		List<Event> toHandle = new ArrayList<Event>();
		for (VM vm : presentVms) {
			List<Event> events = vm.work(nextStop);
			ret.addAll(events);
			toHandle.addAll(events);
		}
		for (Event event : toHandle) {
			ret.addAll(event.apply(this));
		}
		setTime(nextStop);
		log().debug("work on {} till {} : {} events {}",
				new Object[] { getId(), nextStop, ret.size(), ret });
		return ret;
	}

	private void setTime(long time) {
		this.time = time;
	}

	@Override
	public List<Event> handleRequestUnhandled(VMSLAViolation event) {
		return Collections.emptyList();
	}

	@Override
	public List<Event> handleSpecChange(VMSpecChange vmSpecChange) {
		// do nothing in most case, but if this is a memory increase that asks
		// too much memory, suspend the VM and set the level to a warning

		long memdiff = vmSpecChange.vm.getMemNeeds()
		- vmSpecChange.before.getMemNeeds();
		if (memdiff > 0) {
			long memused = getVMsAllocatedMem();
			if (memused + memdiff > getVMsDedicatedMem()) {
				vmSpecChange.setState(EventStates.WARNING);
				suspendVM(vmSpecChange.vm);
				return Arrays.asList(new Event[] { new VMStateChanged(
						vmSpecChange.vm, vmSpecChange.getTime(),
						VMStates.RUNNING, VMStates.SUSPENDED) });
			}
			vmSpecChange.setState(EventStates.RESOLVED);
		}
		return Collections.emptyList();
	}

	@Override
	public List<Event> handleStateChanged(VMStateChanged vmStateChanged) {
		if (vmStateChanged.after == VMStates.STOPPED) {
			vmStateChanged.setState(EventStates.RESOLVED);
			presentVms.remove(vmStateChanged.vm);
		}
		return Collections.emptyList();
	}

	/**
	 * do nothing. an hypervisor should not be notified of such an event, since
	 * those events are only created by hypervisors.
	 */
	@Override
	public List<Event> handleMove(VMMove vmMoved) {
		return Collections.emptyList();
	}

	@Override
	public void restart(long time) {
		presentVms.clear();
		suspendedVMs.clear();
		setTime(time);
	}

	@Override
	public boolean startVM(VM vm) {
		if (presentVms.add(vm)) {
			addTriggeredEvent(new VMMove(vm, getTime(), this,
					VMMoves.NEWRUNNING));
			suspendedVMs.remove(vm);
			return true;
		}
		boolean ret = suspendedVMs.remove(vm);
		if (ret) {
			addTriggeredEvent(new VMStateChanged(vm, getTime(),
					VMStates.SUSPENDED, VMStates.RUNNING));
		}
		return ret;
	}

	@Override
	public boolean suspendVM(VM vm) {
		if (presentVms.add(vm)) {
			suspendedVMs.add(vm);
			addTriggeredEvent(new VMMove(vm, getTime(), this,
					VMMoves.NEWSLEEPING));
			return true;
		} else {
			boolean ret = suspendedVMs.add(vm);
			if (ret) {
				addTriggeredEvent(new VMStateChanged(vm, getTime(),
						VMStates.RUNNING, VMStates.SUSPENDED));
			}
			return ret;
		}
	}

	@Override
	public boolean removeVM(VM vm) {
		suspendedVMs.remove(vm);
		boolean ret = presentVms.remove(vm);
		if (ret) {
			addTriggeredEvent(new VMMove(vm, getTime(), this, VMMoves.DEPARTURE));
		}
		return ret;
	}

	@Override
	public Set<VM> suspendedVMs() {
		return suspendedVMsUnModifiable;
	}

	@Override
	public Set<VM> presentVMs() {
		return presentVMsUnModifiable;
	}
}
